package com.ssau.expert.core.impl;

import com.ssau.expert.common.Question;
import com.ssau.expert.common.QuestionRequestListener;
import com.ssau.expert.common.SuggestedResult;
import com.ssau.expert.core.api.DatabaseCoreAPI;
import com.ssau.expert.core.api.ExpertCoreAPI;
import com.ssau.expert.core.model.Condition;
import com.ssau.expert.core.model.Rule;
import com.ssau.expert.core.model.Variable;
import com.ssau.expert.core.util.ConditionHelper;
import com.ssau.expert.core.util.DataTypeHelper;
import com.ssau.expert.entity.Ship;
import com.ssau.expert.exception.SemanticException;
import com.ssau.expert.util.Names;
import com.ssau.expert.util.QuestionHelper;
import com.sun.istack.internal.Nullable;

import java.util.*;

/**
 * Author: Sergey Saiyan
 * Created: 05.09.13 12:44
 */
public class SuggestHelper {

    protected VariableTable table = VariableTable.get();

    protected Stack<Variable> variableStack = table.variableStack;

    protected DatabaseCoreAPI databaseCoreAPI = DatabaseImpl.get();

    protected ExpertCoreAPI expertCoreAPI = ExpertSystem.get();

    protected Logger logger = Logger.get();

    private Question question = null;

    protected Map<Variable, Collection<Rule>> checkedRulesForVariable = new HashMap<>();

    protected Variable variableForPush = null;

    protected void resetState() {
        question = null;
        checkedRulesForVariable = new HashMap<>();
        variableForPush = null;
        logger.clearLog();
        table.resetTable();
    }

    public SuggestedResult doSuggestion() {
        resetState();
        /* Список всех правил */
        List<Rule> rules = new ArrayList<>(databaseCoreAPI.getRuleCollection());
        Collections.reverse(rules);

        final Variable targetVar = table.getVariable(Names.RESULT);
        /* Помещаем в стек целевую переменную - результат */
        variableStack.push(targetVar);
        logger.info("Целевая переменная 'R' помещена в стек");

        boolean specialExitCondition = false;
        int iteration = 0;

        /* Пока не найдем решение либо что-нибудь не сломается */
        while (!targetVar.isInitialized() && !specialExitCondition) {
            iteration++;
            /* Если что-то отломалось */
            if (iteration == 100) {
                specialExitCondition = true;
                continue;
            }
            /* Правила, которые уже выполняли для данной переменной */
            Collection<Rule> checkedRules = checkedRulesForVariable.get(variableStack.peek());
            if (checkedRules == null) {
                checkedRules = new ArrayList<>();
                checkedRulesForVariable.put(variableStack.peek(), checkedRules);
            }

            /* Находим правило, имеющее := для целевой переменной */
            logger.info("Поиск правила, изменяющего переменную '%s'", variableStack.peek().getName());
            Rule targetRule = getTargetRule(rules, checkedRules);
            /* Если ни одного нет - система несовместна */
            if (targetRule == null) {
                logger.error("Не найдено ни одного правила, изменяющего переменную '%s'", variableStack.peek().getName());
                logger.info("Окончание консультации");
                SuggestedResult res = new SuggestedResult();
                res.setError("Не найдено ни одного правила, изменяющего значение целевой переменной '"
                        + variableStack.peek().getName() + "'");
                return res;
            }

            logger.info("Найдено правило : '%s'", targetRule.getRuleString());
            logger.info("Начинается проверка конъюнктов в правиле");
            /* Проверяем истинно ли условие IF в правиле */
            CheckConjunctResult result = checkRuleConjunct(targetRule);
            /*
            *  Если проверка остановлена из за неиницализированной переменной - переводим эксперта в
            *  ожидание ввода от пользователя
            */
            while (result == CheckConjunctResult.QUESTION) {
                logger.info("В правиле найдена неинициализированная переменная '%s'", question.getVariableName());
                logger.info("Ожидание инициализации переменной пользователем");
                expertCoreAPI.setState(ExpertSystem.State.WAITING_INPUT);
                QuestionRequestListener listener = expertCoreAPI.getListener();
                String answer = listener.onQuestionRequest(question, true);
                if (answer == null) {
                    logger.warn("Пользователь прервал консультацию");
                    SuggestedResult sr = new SuggestedResult();
                    sr.setError("Пользователь прервал консультацию");
                    return sr;
                }

                logger.info("Ответ пользователя для инициализации переменной : '%s'", answer);
                /* Пока пользователь вводит фигню, повторяем вопрос */
                while (!checkAnswer(question, answer)) {
                    logger.warn("Некорректные данные, повторение вопроса");
                    answer = listener.onQuestionRequest(question, false);
                }
                expertCoreAPI.setState(ExpertSystem.State.INITIALIZED);
                result = checkRuleConjunct(targetRule);
            }

            /* Если в ходе проверки условия была найдена неинициализированная переменная,
            *  которую нельзя спросить у пользователя, пушим ее в стек */
            if (result == CheckConjunctResult.NEW_VAR) {
                logger.info("Помещение переменной '%s' в стек", variableForPush.getName());
                variableStack.push(variableForPush);
                continue;
            }

            /**
             *  Если условие проверено и вопросов нет выполним THEN выражение
             */
            if (result == CheckConjunctResult.TRUE) {
                logger.info("Условие правила истинно. Выполняем часть THEN : '%s'", targetRule.getRuleOutput());
                executeOutput(targetRule.getRuleOutput());
                logger.info("Выталкиваем переменную '%s' из стека", variableStack.peek().getName());
                /* Выкидываем верхушку стека */
                variableStack.pop();

                Variable newVar = variableStack.isEmpty() ? null : variableStack.peek();
                /* Если стек пустой - все закончилось */
                if (newVar == null) {
                    logger.info("Стек пуст");
                    return wrapResult(targetVar);
                } else {
                    /* Если стек не пустой - вертим алгоритм дальше*/
                    logger.info("Переменная на вершине стека : '%s'", newVar.getName());
                }
            } else if (result == CheckConjunctResult.FALSE) {
                checkedRules.add(targetRule);
                logger.info("Условие '%s' ложно", targetRule.getConditionsString());
            }
        }

        SuggestedResult result = new SuggestedResult();
        result.setError("Не удалось найти решение. Просмотрите лог для выяснения деталей.");
        return result;
    }

    /* Создает SuggestedResult по строковому значению целевой переменной */
    private SuggestedResult wrapResult(Variable targetVar) {
        String value = targetVar.getValue();
        if (value.contains("[")) {
            value = value.substring(1, value.length() - 1);
        }


        if (value.isEmpty()) {
            return null;
        }

        List<Ship> ships = databaseCoreAPI.getShips();
        if (value.contains(",")) {
            String[] variants = value.split(",");

            SuggestedResult result = new SuggestedResult();
            List<Ship> alternates = new ArrayList<>();
            result.setAlternateShips(alternates);

            for (String variant : variants) {
                if (variant.startsWith("\"")) {
                    variant = variant.substring(1, variant.length() - 1);
                }
                Ship foundShip = null;
                for (Ship ship : ships) {
                    if (ship.getName().equalsIgnoreCase(variant)) {
                        foundShip = ship;
                        break;
                    }
                }
                if (foundShip == null) {
                    throw new RuntimeException("Unknown ship : " + variant);
                }
                alternates.add(foundShip);
            }
            return result;
        } else {
            SuggestedResult result = new SuggestedResult();
            if (value.startsWith("\"")) {
                value = value.substring(1, value.length() - 1);
            }
            Ship foundShip = null;
            for (Ship ship : ships) {
                if (ship.getName().equalsIgnoreCase(value)) {
                    foundShip = ship;
                    break;
                }
            }
            if (foundShip == null) {
                throw new RuntimeException("Unknown ship : " + value);
            }
            result.setSuggestedShip(foundShip);
            return result;
        }
    }

    //TODO мне лень писать здесь проверки, если правило кривое - все упадет.
    /* Выполняет вывод, описанный в правиле */
    private void executeOutput(String ruleOutput) {
        String[] args = ruleOutput.split(" ");
        String var = args[0].trim();
        String value = args[2].trim();
        Variable variable = table.getVariable(var);
        variable.setValue(value);
    }

    /**
     * Проверяет, корректен ли ответ пользователя
     * Если ответ корректен, сразу же инициализирует переменную в вопросе.
     *
     * @param question Вопрос
     * @param answer   Ответ пользователя в виде строки
     * @return TRUE если все ОК
     */
    private boolean checkAnswer(Question question, String answer) {
        try {
            Variable variable = table.getVariable(question.getVariableName());
            Class clazz = variable.getClazz();
            Class vClass = DataTypeHelper.getClassByValue(answer);
            if (!vClass.equals(clazz)) {
                return false;
            }
            if (String.class.equals(vClass)) {
                answer = answer.toUpperCase();
            }
            variable.setValue(answer);
            logger.info("Присваивание значения '%s' переменной '%s'", answer, variable.getName());

            return true;
        } catch (SemanticException ex) {
            return false;
        }
    }

    /**
     * Из списка правил возвращает подходящее, содержащее целевую переменную
     */
    @Nullable
    protected Rule getTargetRule(List<Rule> rules, Collection<Rule> excluded) {
        Rule targetRule = null;
        for (Rule rule : rules) {
            Variable outVar = rule.getOutputVariable();
            if (outVar.equals(variableStack.peek()) && !excluded.contains(rule)) {
                targetRule = rule;
                break;
            }
        }
        return targetRule;
    }

    /**
     * Проверка, истино ли условие в правиле
     */
    protected CheckConjunctResult checkRuleConjunct(Rule rule) {
        boolean conjunctIsTrue = false;
        /* Проверяем каждое условие (связаны &&) */
        for (Condition condition : rule.getConditions()) {
            /* Переменная в условии */
            Variable conditionVar = condition.getVariable();
            /* Если не инициализирована - надо спросить у пользователя */
            if (!conditionVar.isInitialized()) {
                String questionString = QuestionHelper.getQuestionByVariableName(conditionVar.getName());
                /* Если можно спросить у пользователя */
                if (questionString != null) {
                    question = new Question(questionString);
                    question.setVariableName(conditionVar.getName());
                    return CheckConjunctResult.QUESTION;
                } else {
                    variableForPush = conditionVar;
                    return CheckConjunctResult.NEW_VAR;
                }
            } else {
                /* Если инициализирована - проверим истинность */
                Object value = conditionVar.getValue();
                if (value == null) {
                    throw new RuntimeException(String.format("Variable '%s' is marked as initialized, but value is null",
                            conditionVar.getName()));
                }
                boolean isTrue = ConditionHelper.isTrue(condition);
                if (isTrue) {
                    conjunctIsTrue = true;
                } else {
                    conjunctIsTrue = false;
                    break;
                }
            }
        }
        return conjunctIsTrue ? CheckConjunctResult.TRUE : CheckConjunctResult.FALSE;
    }

    public static SuggestHelper get() {
        return instance;
    }

    private static SuggestHelper instance = new SuggestHelper();

    protected enum CheckConjunctResult {
        QUESTION,
        TRUE,
        FALSE,
        NEW_VAR
    }
}
